iT邦幫忙

2023 iThome 鐵人賽

DAY 4
0
Software Development

Haskell 從入門到放棄系列 第 4

[Haskell 從入門到放棄] Day04 - 簡簡單單的List

  • 分享至 

  • xImage
  •  

Day 04 | 簡簡單單的List

今天要來介紹不管在哪個語言都是非常常用的資料結構 List ,在 Haskell 中 List 中每個元素只能是同樣型別

-- ⭕️
[1,2,3,4,5,6]
['a','b','c']

-- ❌
[1,2,'c',4,'a',6]
[1,'b','c']

接下來讓我們來看 Haskell 裡 List 的各種操作吧

基本操作

在 Haskell 中 List 的宣告方式為

-- in ghci

x = [1,2,3,4,5,6,7]

x -- [1,2,3,4,5,6,7]

有一個比較重要的概念是

在 Haskell 中 String[Char] 是相等的

也就是說"abc" 以及 ['a','b','c'] 是一樣的東西

['a','b','c'] == "abc"

List 常用到的運算子

如果我們今天要串接 List 我們可以使用 ++

[1,2,3,4] ++ [5,6] -- [1,2,3,4,5,6]
['a','b'] ++ ['c']  -- "abc"
"ab" ++ "c" -- "abc"

在使用 ++ 要特別注意的是他是會將左邊運算元遍歷過一次,所以如果左邊 是一個很長的 List 的話必須警慎使用。

那如果我們是要在 List 前面塞入一個元素可以使用 :

特別注意一下這裡是 'a' 而不是 "a" 差異為一個是 Char[Char] (String) 。

'a' : "bc" -- "abc"
1 : [2,3,4] -- [1,2,3,4]

我們也可以一直使用 : 來組成一個 List

'f' : 'o' : 'o' : [] -- "foo"
0 : 1 : 2 : [] -- [0,1,2]

其實 Haskell 在建立 List 時就是不斷遞迴用 : 把左邊運算子不斷的塞入來建立出 List 的,所以[0,1,2] 實際上就是 0 : 1 : 2 : [] 的語法糖。

那如果我們要存取特定的元素的話我們能用 !!

[1,2,3,4,5,6] !! 0 -- 1

"abc" !! 0 -- 'a'
"abc" !! 1 -- 'b'

常用 function

x = [1,2,3,4,5,6]
head x -- 1
tail x -- [2,3,4,5,6]
last x -- 6
init x -- [1,2,3,4,5]

head 會回傳 List 中第一個元素

tail 會回傳扣除第一個元素後的List

last 會回傳 List 的最後一個元素

init 會回傳扣除最後一個元素後的List

有這些我們就能夠來寫出一個簡單小 function 來操作 List

listSum :: [Int] -> Int
listSum x = if null x then 0 else head x + listSum (tail x) 

main :: IO ()
main = do
  let x = [1,2,3,4,5,6,7,8,9,10]
  putStrLn (show(listSum x))

這裡我們使用了 headtail 協助我們加總 List 中的所有元素,listSum 解釋起來就是當 x 不為 null 時就會將 x 的第一個元素與listSum (tail x) 相加。

沒錯就是使用遞迴,因為在 Haskell 中我們所有 variable 都是 immutable 的所以我們寫不出像 js 這樣的程式碼

a = 0
list = [1,2,3,4,5,6,7,8,9,10]
list.forEach(x => a += x)

大家先有一個概念就是 Haskell 上的在定義變數時 = 可以用數學上的「等於」來解釋,在數學上我們不可能說有一個「數值 x」是等於 x + 1 吧。

像是我先定義了 x = 5,不可能又有一個等式說明了 x = 6,所以也不會發生 x = x + 1 也就是5 = 6 了這件事情了。

當然,如果是描述函式關係的話這樣 x = x + 1 是非常合理的描述

除了上面四個幫我們快速找到頭尾的function 以外 Haskell 還有提供其他好用的 funtion

length [1,2,3,4,5] -- 5 
reverse [1,2,3,4,5] -- [5,4,3,2,1]
take 2 [1,2,3,4,5] -- [1,2]
maximum [1,2,3,4,5] -- 5
minimum [1,2,3,4,5] -- 1
product [1,2,3,4,5] -- 120
sum [1,2,3,4,5] -- 15
elem 4 [1,2,3,4,5] -- True

length 會回傳 List 長度

take 會根據根據傳入的長度及List 回傳一個新的 List

reverse 會回傳一個反轉的 List

maximum 回傳 List 中最大的數

minimum 回傳 List 中最小的數

sum List所有元素的加總

product List所有元素的乘積

elem 判斷該元素是否屬於該List

通常我們都是使用 4 elem [1,2,3,4,5]
這種 infix 的形式在使用 elem

稍微進階一下

當然我們要建立一個 List 不一定每次都要直接寫出所有的元素

x = [1,2,3,4]

我們可使用 .. 來幫我快速建立一個 List

[1..10] -- [1,2,3,4,5,6,7,8,9,10]
['a'..'z']  -- "abcdefghijklmnopqrstuvwxyz"

這種方式稱為 range ,而能夠使用 range 來建立 List 那個值本身必須是可以被枚舉的。像是A到Z,1到10。

那我們也能限制 range 所產生的元素之間的距離,只要多放一個元素且標註上限就好

[2,4..20] -- [2,4,6,8,10,12,14,16,18,20]
[5,10..49] -- [5,10,15,20,25,30,35,40,45]

那如果我們不限制 range 上限呢?那我們就可以產生出一個無限的 List 。

[1..] -- 這會產生一個無限 List 如果要在ghci停止請 使用 command+c 來停止

所以假設我們今天想產生前17個17的倍數,我們除了直接標註上限是17*100以外,我們也可以利用 take 加上無限 List 來幫助我們達成這個需求

take 17 [17,34..] 
-- [17,34,51,68,85,102,119,136,153,170,187,204,221,238,255,272,289]

除了使用 range 我們也可以用 cycle 以及 repeat 來產生無限 List

take 5 (cycle "abc") -- "abcab"
take 5 (repeat 'a') -- "aaaaa"

-- 上面的 repeat 的寫法可以使用 replicate 簡化
replicate 5 'a' -- "aaaaa"

今天的程式碼一樣會放到 github


上一篇
[Haskell 從入門到放棄] Day 03 - 基礎語法
下一篇
[Haskell 從入門到放棄] Day 05 - List Comprehension
系列文
Haskell 從入門到放棄30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
shootingstar
iT邦新手 4 級 ‧ 2023-09-30 14:21:01

補充

不要使用partial function

所謂的partial function就是傳入某些合法的值卻回傳bottom的function。合法的值指這個function定義的input type的值域,例如Int裡就只有數字;bottom就是會讓程式壞掉的值,例如拋出exception。

Haskell預設import的function裡就包含著這些function,例如headtailinit,只要傳empty list進去程式就會壞掉,而Haskell在處理exception的議題上非常非常棘手,原因就是它沒有stack trace,你絕對不會希望在production上程式丟出exception然後查不出原因(Haskell的stack trace有三種不同的產生方式,但沒有一個完美的做法,要嘛可讀性差,要嘛會影響效能,要嘛stack trace中間會斷掉)。

相關的partial function可以參考haskell-dangerous-functions

List不是唯一的資料結構

Haskell一家獨大list,什麼東西都用list表示,唯一內建的資料結構只有list,但它是linked list呀!還是單向的!它的insert和index操作都是O(n),你呼叫一次length也是O(n),它可不會把list長度cache起來。String用a list of characters表示也實在不是明智的做法,試想一下在Java裡你會用LinkedList<Character>來表示字串嗎?不會呀!Character被box過,要間接由pointer去存取,這在Haskell也是,不僅佔空間又影響效能。

所以請考慮一下使用別的資料結構,以下為幾乎被認為是標準的幾個函式庫(Haskell裡實在是不存在真正標準的函式庫,內建的唯一函式庫base功能是真的少):

  • array:API有點難用,而且都是partial function
  • vector:類比Java的ArrayList,不想用上面的array可以考慮,有immutable及mutable版,使用immutable版時要注意盡量不要做修改的操作,它會複製一個新的出來
  • dlist:list的append操作是O(n),如果你的append很多的話可以考慮使用此函式庫,它是O(1)
  • containers:Map、Set可以類比Java的TreeMap、TreeSet,Seq在random access及append操作上算是比較好的資料結構,而且它的修改又不需要整個重新複製一遍,算是vector和list的折衷方案
  • unorder-containers:提供HashMap和HashSet
  • text:String的替代方案
  • bytestring:相當於byte array

上面的資料結構通常又有分lazy和strict的版本,我個人是認為沒什麼特別需求就都選strict的版本,lazy會堆一些尚未evaluated的程式區塊,我們稱作thunk,這東西是造成memory space leak的元兇。

這裡附上幾個資料結構的benchmark,可以參考這個來選資料結構:
https://github.com/haskell-perf/sequences
https://github.com/haskell-perf/dictionaries

非常感謝大大的補充
也許是我並沒有真正拿 Haskell 開發過所以其實我並沒有認真看待過效能及例外處理的問題。

那我是不是能理解 Haskell 的 String 算是一種設計錯誤?像是其他語言中可能是分配一塊記憶體位置來作存放

Haskell的String我想大多數人都會認為它是設計錯誤,String除了可以直接拿List的function來用之外,也沒其他好處了。

Haskell預設import的module Prelude基本上也是設計錯誤,裡面的partial function應該要全部拿掉才是,於是就有一大堆的函式庫弄了自己的Prelude版本想取而代之,其中也有把String全部換成Text的例子。

不過一改就是breaking change,一堆函式庫都會壞掉的。

我要留言

立即登入留言